// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storage

import (
	"context"
	"time"

	"github.com/juju/names/v6"

	"github.com/juju/juju/apiserver/authentication"
	apiservererrors "github.com/juju/juju/apiserver/errors"
	"github.com/juju/juju/apiserver/facade"
	coreerrors "github.com/juju/juju/core/errors"
	corelogger "github.com/juju/juju/core/logger"
	coremodel "github.com/juju/juju/core/model"
	"github.com/juju/juju/core/permission"
	coreunit "github.com/juju/juju/core/unit"
	coreversion "github.com/juju/juju/core/version"
	domainremoval "github.com/juju/juju/domain/removal"
	domainstorage "github.com/juju/juju/domain/storage"
	storageerrors "github.com/juju/juju/domain/storage/errors"
	domainstorageprovisioning "github.com/juju/juju/domain/storageprovisioning"
	"github.com/juju/juju/internal/errors"
	"github.com/juju/juju/rpc/params"
)

// RemovalService defines the interface required for removing storage related
// entities in the model on behalf of an API caller.
type RemovalService interface {
	// RemoveStorageAttachment is responsible for removing a storage attachment
	// from a unit. If the unit is Alive then removing this storage attachment
	// must not violate the storage requirements of the charm.
	//
	// The following errors may be returned:
	// - [coreerrors.NotValid] if the storage attachment uuid is not valid.
	// - [storageerrors.StorageAttachmentNotFound] if the storage attachment
	// does not exist in the model.
	// - [applicationerrors.UnitStorageMinViolation] if removing a storage
	// attachment would violate the charm minimums required for the unit.
	RemoveStorageAttachment(
		ctx context.Context,
		uuid domainstorageprovisioning.StorageAttachmentUUID,
		force bool,
		wait time.Duration,
	) (domainremoval.UUID, error)

	// RemoveStorageInstance ensures that the specified storage instance is no
	// longer alive, scheduling removal jobs if needed and if specified, mark the
	// volume and filesystems for obliteration.
	RemoveStorageInstance(
		ctx context.Context,
		uuid domainstorage.StorageInstanceUUID,
		force bool, wait time.Duration,
		obliterate bool,
	) error
}

// StorageService defines apis on the storage service.
type StorageService interface {
	// GetStorageAttachmentUUIDForStorageInstanceAndUnit returns the
	// [domainstorageprovisioning.StorageAttachmentUUID] associated with the
	// given storage instance id and unit name.
	//
	// The following errors may be returned:
	// - [github.com/juju/juju/domain/storage/errors.StorageNotFound] if the
	// storage instance for the supplied uuid no longer exists.
	// - [github.com/juju/juju/domain/application/errors.UnitNotFound] if the
	// unit no longer exists for the supplied uuid.
	GetStorageAttachmentUUIDForStorageInstanceAndUnit(
		ctx context.Context,
		uuid domainstorage.StorageInstanceUUID,
		unitUUID coreunit.UUID,
	) (domainstorageprovisioning.StorageAttachmentUUID, error)

	// GetStorageInstanceAttachments returns the set of attachments a storage
	// instance has. If the storage instance has no attachments then an empty
	// slice is returned.
	//
	// The following errors may be returned:
	// - [coreerrors.NotValid] when the supplied uuid did not pass validation.
	// - [github.com/juju/juju/domain/storage/errors.StorageInstanceNotFound] if
	// the storage instance for the supplied uuid does not exist.
	GetStorageInstanceAttachments(
		context.Context, domainstorage.StorageInstanceUUID,
	) ([]domainstorageprovisioning.StorageAttachmentUUID, error)

	// GetStorageInstanceUUIDForID returns the StorageInstanceUUID for the given
	// storage ID.
	//
	// The following errors may be returned:
	// - [github.com/juju/juju/domain/storage/errors.StorageNotFound] if no
	// storage instance exists for the provided storage id.
	GetStorageInstanceUUIDForID(
		ctx context.Context,
		storageID string,
	) (domainstorage.StorageInstanceUUID, error)

	// ListStoragePools returns all the storage pools.
	ListStoragePools(ctx context.Context) ([]domainstorage.StoragePool, error)

	// ListStoragePoolsByNamesAndProviders returns the storage pools matching the specified
	// names and providers, including the default storage pools.
	// If no names and providers are specified, an empty slice is returned without an error.
	// If no storage pools match the criteria, an empty slice is returned without an error.
	ListStoragePoolsByNamesAndProviders(
		ctx context.Context, names domainstorage.Names, providers domainstorage.Providers,
	) ([]domainstorage.StoragePool, error)

	// ListStoragePoolsByNames returns the storage pools matching the specified names, including
	// the default storage pools.
	// If no names are specified, an empty slice is returned without an error.
	// If no storage pools match the criteria, an empty slice is returned without an error.
	ListStoragePoolsByNames(
		ctx context.Context, names domainstorage.Names,
	) ([]domainstorage.StoragePool, error)

	// ListStoragePoolsByProviders returns the storage pools matching the specified
	// providers, including the default storage pools.
	// If no providers are specified, an empty slice is returned without an error.
	// If no storage pools match the criteria, an empty slice is returned without an error.
	ListStoragePoolsByProviders(
		ctx context.Context, providers domainstorage.Providers,
	) ([]domainstorage.StoragePool, error)
}

// ApplicationService defines apis on the application service.
type ApplicationService interface {
	// GetUnitUUID returns the UUID for the named unit.
	//
	// The following errors may be returned:
	// - [github.com/juju/juju/core/unit.InvalidUnitName] if the unit name is
	// invalid.
	// - [github.com/juju/juju/domain/application/errors.UnitNotFound] if the
	// unit doesn't exist.
	GetUnitUUID(context.Context, coreunit.Name) (coreunit.UUID, error)
}

// StorageAPIv6 provides the Storage API facade for version 6.
type StorageAPIv6 struct {
	*StorageAPI
}

// StorageAPI implements the latest version (v7) of the Storage API.
type StorageAPI struct {
	applicationService ApplicationService
	removalService     RemovalService
	storageService     StorageService

	authorizer     facade.Authorizer
	controllerUUID string
	modelUUID      coremodel.UUID
	logger         corelogger.Logger
}

func NewStorageAPI(
	controllerUUID string,
	modelUUID coremodel.UUID,
	authorizer facade.Authorizer,
	logger corelogger.Logger,
	applicationService ApplicationService,
	removalService RemovalService,
	storageService StorageService,
) *StorageAPI {
	return &StorageAPI{
		applicationService: applicationService,
		removalService:     removalService,
		storageService:     storageService,

		authorizer:     authorizer,
		controllerUUID: controllerUUID,
		modelUUID:      modelUUID,
		logger:         logger,
	}
}

func (a *StorageAPI) checkCanRead(ctx context.Context) error {
	err := a.authorizer.HasPermission(ctx, permission.SuperuserAccess, names.NewControllerTag(a.controllerUUID))
	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
		return errors.Capture(err)
	}

	if err == nil {
		return nil
	}
	return a.authorizer.HasPermission(ctx, permission.ReadAccess, names.NewModelTag(a.modelUUID.String()))
}

func (a *StorageAPI) checkCanWrite(ctx context.Context) error {
	return a.authorizer.HasPermission(ctx, permission.WriteAccess, names.NewModelTag(a.modelUUID.String()))
}

// StorageDetails retrieves and returns detailed information about desired
// storage identified by supplied tags. If specified storage cannot be
// retrieved, individual error is returned instead of storage information.
func (a *StorageAPI) StorageDetails(ctx context.Context, entities params.Entities) (params.StorageDetailsResults, error) {
	return params.StorageDetailsResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// ListStorageDetails returns storage matching a filter.
func (a *StorageAPI) ListStorageDetails(ctx context.Context, filters params.StorageFilters) (params.StorageDetailsListResults, error) {
	return params.StorageDetailsListResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// ListPools returns a list of pools.
// If filter is provided, returned list only contains pools that match
// the filter.
// Pools can be filtered on names and provider types.
// If both names and types are provided as filter,
// pools that match either are returned.
// This method lists union of pools and environment provider types.
// If no filter is provided, all pools are returned.
func (a *StorageAPI) ListPools(
	ctx context.Context,
	filters params.StoragePoolFilters,
) (params.StoragePoolsResults, error) {
	if err := a.checkCanRead(ctx); err != nil {
		return params.StoragePoolsResults{}, errors.Capture(err)
	}

	results := params.StoragePoolsResults{
		Results: make([]params.StoragePoolsResult, len(filters.Filters)),
	}
	for i, filter := range filters.Filters {
		pools, err := a.listPools(ctx, filter)
		if err != nil {
			results.Results[i].Error = apiservererrors.ServerError(err)
			continue
		}
		results.Results[i].Result = pools
	}
	return results, nil
}

func (a *StorageAPI) listPools(ctx context.Context, filter params.StoragePoolFilter) ([]params.StoragePool, error) {
	var (
		pools []domainstorage.StoragePool
		err   error
	)
	if len(filter.Names) == 0 && len(filter.Providers) == 0 {
		pools, err = a.storageService.ListStoragePools(ctx)
	} else if len(filter.Names) != 0 && len(filter.Providers) != 0 {
		pools, err = a.storageService.ListStoragePoolsByNamesAndProviders(ctx, filter.Names, filter.Providers)
	} else if len(filter.Names) != 0 {
		pools, err = a.storageService.ListStoragePoolsByNames(ctx, filter.Names)
	} else {
		pools, err = a.storageService.ListStoragePoolsByProviders(ctx, filter.Providers)
	}
	if err != nil {
		return nil, errors.Capture(err)
	}
	results := make([]params.StoragePool, len(pools))
	for i, p := range pools {
		pool := params.StoragePool{
			Name:     p.Name,
			Provider: p.Provider,
		}
		if len(p.Attrs) > 0 {
			pool.Attrs = make(map[string]any, len(p.Attrs))
			for k, v := range p.Attrs {
				pool.Attrs[k] = v
			}
		}
		results[i] = pool

	}
	return results, nil
}

// CreatePool creates a new pool with specified parameters.
func (a *StorageAPI) CreatePool(ctx context.Context, p params.StoragePoolArgs) (params.ErrorResults, error) {
	return params.ErrorResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// ListVolumes lists volumes with the given filters. Each filter produces
// an independent list of volumes, or an error if the filter is invalid
// or the volumes could not be listed.
func (a *StorageAPI) ListVolumes(ctx context.Context, filters params.VolumeFilters) (params.VolumeDetailsListResults, error) {
	return params.VolumeDetailsListResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// ListFilesystems returns a list of filesystems in the environment matching
// the provided filter. Each result describes a filesystem in detail, including
// the filesystem's attachments.
func (a *StorageAPI) ListFilesystems(ctx context.Context, filters params.FilesystemFilters) (params.FilesystemDetailsListResults, error) {
	return params.FilesystemDetailsListResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// AddToUnit validates and creates additional storage instances for units.
// A "CHANGE" block can block this operation.
func (a *StorageAPI) AddToUnit(ctx context.Context, args params.StoragesAddParams) (params.AddStorageResults, error) {
	return params.AddStorageResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// Remove sets the specified storage entities to Dying, unless they are
// already Dying or Dead, such that the storage will eventually be removed
// from the model. If the arguments specify that the storage should be
// destroyed, then the associated cloud storage will be destroyed first;
// otherwise it will only be released from Juju's control.
func (a *StorageAPI) Remove(ctx context.Context, args params.RemoveStorage) (params.ErrorResults, error) {
	if err := a.checkCanWrite(ctx); err != nil {
		return params.ErrorResults{}, errors.Capture(err)
	}

	one := func(arg params.RemoveStorageInstance) error {
		tag, err := names.ParseStorageTag(arg.Tag)
		if err != nil {
			return errors.New("invalid storage tag").Add(coreerrors.NotValid)
		}
		return a.removeStorageInstance(ctx, tag, arg)
	}

	results := make([]params.ErrorResult, 0, len(args.Storage))
	for _, v := range args.Storage {
		var result params.ErrorResult
		err := one(v)
		if err != nil {
			result.Error = apiservererrors.ServerError(err)
		}
		results = append(results, result)
	}
	return params.ErrorResults{Results: results}, nil
}

// removeStorageInstance performs the operations to remove a storage instance
// for the corresponding Remove facade method.
func (a *StorageAPI) removeStorageInstance(
	ctx context.Context, tag names.StorageTag, arg params.RemoveStorageInstance,
) error {
	force := false
	if arg.Force != nil {
		force = *arg.Force
	}
	wait := time.Duration(0)
	if arg.MaxWait != nil {
		wait = *arg.MaxWait
	}
	if wait < 0 {
		return errors.Errorf(
			"max wait time cannot be a negative number",
		).Add(coreerrors.NotValid)
	}

	uuid, err := a.storageService.GetStorageInstanceUUIDForID(ctx, tag.Id())
	if errors.Is(err, storageerrors.StorageInstanceNotFound) {
		return errors.Errorf("storage %q does not exist", tag.Id()).Add(
			coreerrors.NotFound,
		)
	} else if err != nil {
		return errors.Errorf(
			"getting storage instance uuid for storage id %q: %w",
			tag.Id(), err,
		)
	}

	if arg.DestroyAttachments {
		saUUIDs, err := a.storageService.GetStorageInstanceAttachments(
			ctx, uuid)
		if errors.Is(err, storageerrors.StorageInstanceNotFound) {
			return errors.Errorf("storage %q does not exist", tag.Id()).Add(
				coreerrors.NotFound,
			)
		} else if err != nil {
			return errors.Errorf(
				"getting attachments of storage instance %q: %w",
				tag.Id(), err,
			)
		}
		for _, saUUID := range saUUIDs {
			err := a.detachStorageAttachment(ctx, saUUID, force, wait)
			if err != nil {
				return errors.Errorf(
					"removing storage attachment %q for storage %q:",
					saUUID, tag.Id(),
				)
			}
		}
	}

	obliterate := arg.DestroyStorage
	err = a.removalService.RemoveStorageInstance(
		ctx, uuid, force, wait, obliterate)
	if errors.Is(err, storageerrors.StorageInstanceNotFound) {
		return errors.Errorf("storage %q does not exist", tag.Id()).Add(
			coreerrors.NotFound,
		)
	} else if err != nil {
		return errors.Errorf("removing storage %q: %w", tag.Id(), err)
	}

	return nil
}

// Attach attaches existing storage instances to units.
// A "CHANGE" block can block this operation.
func (a *StorageAPI) Attach(ctx context.Context, args params.StorageAttachmentIds) (params.ErrorResults, error) {
	return params.ErrorResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// Import imports existing storage into the model.
// A "CHANGE" block can block this operation.
func (a *StorageAPI) Import(ctx context.Context, args params.BulkImportStorageParamsV2) (params.ImportStorageResults, error) {
	return params.ImportStorageResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// Import imports existing storage into the model.
// A "CHANGE" block can block this operation.
func (a *StorageAPIv6) Import(ctx context.Context, args params.BulkImportStorageParams) (params.ImportStorageResults, error) {
	return params.ImportStorageResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// RemovePool deletes the named pool
func (a *StorageAPI) RemovePool(ctx context.Context, p params.StoragePoolDeleteArgs) (params.ErrorResults, error) {
	return params.ErrorResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}

// UpdatePool deletes the named pool
func (a *StorageAPI) UpdatePool(ctx context.Context, p params.StoragePoolArgs) (params.ErrorResults, error) {
	return params.ErrorResults{}, apiservererrors.ParamsErrorf(
		params.CodeNotYetAvailable, "not yet available in %s", coreversion.Current.String(),
	)
}
